{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 3\n",
    "\n",
    "### <center> Автор материала: Лозовой Виталий Витальевич"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Прогнозирование ориентации по анкете на сайте знакомств </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**План исследования**\n",
    " - Описание набора данных и признаков\n",
    " - Первичный анализ признаков\n",
    " - Первичный визуальный анализ признаков\n",
    " - Закономерности, \"инсайты\", особенности данных\n",
    " - Предобработка данных\n",
    " - Создание новых признаков и описание этого процесса\n",
    " - Кросс-валидация, подбор параметров\n",
    " - Построение кривых валидации и обучения \n",
    " - Прогноз для тестовой или отложенной выборки\n",
    " - Оценка модели с описанием выбранной метрики\n",
    " - Выводы\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time, os\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "from matplotlib import pyplot as plt\n",
    "from bs4 import BeautifulSoup\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "from sklearn.linear_model import LogisticRegression, LogisticRegressionCV\n",
    "from sklearn.preprocessing import LabelEncoder, LabelBinarizer, StandardScaler, OneHotEncoder\n",
    "from sklearn.metrics import f1_score, make_scorer, roc_auc_score, accuracy_score, recall_score\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.cross_validation import StratifiedKFold, train_test_split\n",
    "from sklearn.model_selection import GridSearchCV, cross_val_score, validation_curve, learning_curve\n",
    "from scipy.sparse import csr_matrix, hstack\n",
    "import catboost\n",
    "\n",
    "import tensorflow as tf\n",
    "import tensorflow_hub as hub\n",
    "\n",
    "%matplotlib inline\n",
    "\n",
    "# константа для задания размеров графиков\n",
    "FIGSIZE_TUPLE = (12, 9)\n",
    "\n",
    "# настройка внешнего вида графиков в seaborn\n",
    "sns.set_context(\n",
    "    \"notebook\", \n",
    "    font_scale = 1.5,      \n",
    "    rc = { \n",
    "        \"figure.figsize\" : FIGSIZE_TUPLE, \n",
    "        \"axes.titlesize\" : 18 \n",
    "    }\n",
    ")\n",
    "\n",
    "sns.set_style('darkgrid')\n",
    "sns.set(rc={'figure.figsize':FIGSIZE_TUPLE})\n",
    "from matplotlib import rcParams\n",
    "rcParams['figure.figsize'] = FIGSIZE_TUPLE\n",
    "\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 1. Описание набора данных и признаков\n",
    "\n",
    "Ссылка на скачивание https://github.com/rudeboybert/JSE_OkCupid/raw/master/profiles.csv.zip\n",
    "\n",
    "Название датасета: San Francisco OKCupid Users\n",
    "Размер: таблица на 59946 анкет (пользователей, активных на 30.06.2012), 31 переменная\n",
    "\n",
    "Описание: Датасет создан скриптом, собирающим данные открытых публичных профилей на www.okcupid.com. 59946 анкет пользователей живущих в радиусе не более 25 миль от Сан Франциско, которые были онлайн в течении года (с 30.06.2011), с хотя бы одной фотографией.\n",
    "Разрешение на использование данных получено у президента и сооснователя OkCupid - Christian Rudder.\n",
    "\n",
    "Описание переменных:\n",
    "Некоторые столбцы могут иметь 2 значения. В таких случаях значения разделяются точкой с запятой (например \"graduated from; two-year college\").\n",
    "\n",
    "Столбцы:\n",
    "\n",
    "| Столбец | Пояснение, тип | Значения |\n",
    "|:---------:|:------:| ----------- |\n",
    "| age | Возраст, чисельный | Значение в годах |\n",
    "| body_type | Телосложение, категориальный | rather not say, thin, overweight, skinny, average, fit, athletic, jacked, a little extra, curvy, full figured, used up |\n",
    "| diet | Диета, категориальный | mostly/strictly; anything, vegetarian, vegan, kosher, halal, other |\n",
    "| drinks | Отношение к алкоголю, категориальный | very often, often, socially, rarely, desperately, not at all |\n",
    "| drugs | Употребление веществ, категориальный | never, sometimes, often |\n",
    "| education | Образование, категориальный, может иметь несколько значений | graduated from, working on, dropped out of; high school, two-year college, university, masters program, law school, med school, Ph.D program, space camp |\n",
    "| ethnicity | Происхождение | Asian, middle eastern, black, native American, indian, pacific islander, Hispanic/latin, white, other |\n",
    "| height | Рост, чисельный | Значение задано в дюймах |\n",
    "| income | Доход, как не странно - категориальный. Задан в US $, -1 значит \"лучше даже не говорить\" | -1, 20000, 30000, 40000, 50000, 60000 70000, 80000, 100000, 150000, 250000, 500000, 1000000 |\n",
    "| job | Род деятельности, категориальный | student, art/music/writing, banking/finance, administration, technology, construction, education, entertainment/media, management, hospitality, law, medicine, military, politics/government, sales/marketing, science/engineering, transportation, unemployed, other, rather not say, retire |\n",
    "| last_online | Дата и время последнего посещения, строка | Дата в формате ГГГГ-ММ-ДД-ЧЧ-мм |\n",
    "| offspring | Наличие/отношение к детям. Может иметь 2 значения | has a kid, has kids, doesnt have a kid, doesn't want kids; ,and/,but might want them, wants them, doesnt want any, doesnt want more |\n",
    "| orientation | Сексуальная ориентация, категориальный | straight, gay, bisexual |\n",
    "| pets | Коты, собаки и отношение к ним - может иметь 2 значения | has dogs, likes dogs, dislikes dogs; and has cats, likes cats, dislikes cats |\n",
    "| religion | Религия и отношение к ней, категориальный, может иметь 2 значения | agnosticism, atheism, Christianity, Judaism, Catholicism, Islam, Hinduism, Buddhism, Other; and very serious about it, and somewhat serious about it, but not too serious about it, and laughing about it |\n",
    "| sex | Пол, бинарный | m, f |\n",
    "| sign | Знак зодиака и отношение к нему, категориальный, может иметь 2 значения | aquarius, pices, aries, Taurus, Gemini, cancer, leo, virgo, libra, scorpio, saggitarius, Capricorn; but it doesn’t matter, and it matters a lot, and it’s fun to think about |\n",
    "| smokes | Курение, категориальный | yes, sometimes, when drinking, trying to quit, no |\n",
    "| speaks | Языки, категориальный, может иметь много значений | English (fluently, okay, poorly). Afrikaans, Albanian, Arabic, Armenian, Basque, Belarusan, Bengali, Breton, Bulgarian, Catalan, Cebuano, Chechen, Chinese, C++, Croatian, Czech, Danish, Dutch, Esperanto, Estonian, Farsi, Finnish, French, Frisian, Georgian, German, Greek, Gujarati, Ancient Greek, Hawaiian, Hebrew, Hindi, Hungarian, Icelandic, Ilongo, Indonesian, Irish, Italian, Japanese, Khmer, Korean, Latin, Latvian, LISP, Lithuanian, Malay, Maori, Mongolian, Norwegian, Occitan, Other, Persian, Polish, Portuguese, Romanian, Rotuman, Russian, Sanskrit, Sardinian, Serbian, Sign Language, Slovak, Slovenian, Spanish, Swahili, Swedish, Tagalog, Tamil, Thai, Tibetan, Turkish, Ukranian, Urdu, Vietnamese, Welsh, Yiddish (fluently, okay, poorly) |\n",
    "| status | Семейное положение, категориальный | single, seeing someone, married, in an open relationship |\n",
    "\n",
    "Есть еще набор текстовых полей - essay0-9\n",
    "\n",
    "  - essay0 - \"My self summary\" - кратко о себе\n",
    "  - essay1 - \"What I’m doing with my life\" - чем я занимаюсь\n",
    "  - essay2 - \"I’m really good at\" - что я делаю хорошо\n",
    "  - essay3 - \"The first thing people usually notice about me\" - первое впечатление\n",
    "  - essay4 - \"Favorite books, movies, show, music, and food\" - любимые продукты массовой культуры\n",
    "  - essay5 - \"The six things I could never do without\" - вещи без которых я не могу обойтись\n",
    "  - essay6 - \"I spend a lot of time thinking about\" - много думаю о ...\n",
    "  - essay7 - \"On a typical Friday night I am\" - как я отдыхаю\n",
    "  - essay8 - \"The most private thing I am willing to admit\" - что-то личное что я могу отметить\n",
    "  - essay9 - \"You should message me if...\" - напиши мне если ...\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = pd.read_csv(\"../../data/okcupid_profiles.csv\")\n",
    "data.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 2. Первичный анализ признаков"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Взглянем на столбцы\n",
    "data.columns"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Как выглядят даные?\n",
    "data.head().T"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Очень важно знать заполненьость даных\n",
    "(data.count()/data.shape[0] * 100).sort_values(ascending=False).map('{:,.2f}'.format)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим - 4 поля, видимо, являются обязательными, last_online и location заполнялся парсером поетому заполнены на 100%. Категории достаточно подробно описаны, поетому посмотрим как выглядят столбцы, где можно выбрать несколько значений, и распределения роста и возраста"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data[['height', 'age']].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Рост задан в дюймах и, как можно видеть, даные не очень чистые - есть и дюймовочка, и 2.5-метровый человек. По возрасту так-же есть выбросы, обработаем выбросы, и посмотрим как выглядит наш целевой признак <b>ориентация</b>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['sex'], data['orientation'], margins=True)\n",
    "ct"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь то же самое, но в процентном соотношении"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct[ct.columns[:-1]]/np.tile(ct['All'].values, (3,1)).T*100"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['diet'].unique()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Пока все просто - гастрономические предпочтения заданы категорией и \"насколько строга диета\" - `<strictly|normal|mostly>`. Разобъем диету на 2 колонки и заполним пробелы модой"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def diet_transformer(x):\n",
    "  if type(x) != list:\n",
    "    return [None, None]\n",
    "  if len(x) == 1:\n",
    "    x.append('default')\n",
    "    return x\n",
    "  else:\n",
    "    return x[::-1]\n",
    "spltd = data['diet'].str.split(' ', 2).transform(diet_transformer)\n",
    "spltd = pd.DataFrame(spltd.tolist(), columns=['diet_kind', 'diet_strenght'])\n",
    "data['diet_kind'] = spltd['diet_kind'].fillna(spltd['diet_kind'].mode().values[0])\n",
    "data['diet_strenght'] = spltd['diet_strenght'].fillna(spltd['diet_strenght'].mode().values[0])\n",
    "data['diet_kind'].unique(), data['diet_strenght'].unique()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['education'].unique()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['ethnicity'].unique()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Судя по ответам сродни **'asian, black, native american, pacific islander, white'** фича `etnicity` анализировать будет сложно"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['religion'].unique()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Очень много вариантов, попробуем разбить на 2 признака - религия, отношение к религии"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "spltd = data['religion'].str.split(' ', 1).transform(lambda x: [None, None] if type(x) != list else x)\n",
    "data = pd.concat([data, pd.DataFrame(spltd.tolist(), columns=['religion_confession', 'religion_respect'])], axis=1)\n",
    "data['religion_confession'].unique(), data['religion_respect'].unique()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['offspring'].unique()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Признак состоит по сути из двух - категории \"есть ребенок/дети/нет детей\" и категории \"хочу/возможно захочу/не хочу\". Разделим его в 2 столбца"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def child_presense_transformer(x):\n",
    "  if type(x) == float: #it's a NaN, handle it later\n",
    "    return x\n",
    "  elif x.startswith('has kids'):\n",
    "    return 'multiple'\n",
    "  elif x.startswith('has a kid'):\n",
    "    return 'one'\n",
    "  else:\n",
    "    return 'no'\n",
    "  \n",
    "def want_child_transformer(x):\n",
    "  if type(x) == float: #it's a NaN, handle it later\n",
    "    return x\n",
    "  elif 'wants' in x:\n",
    "    return 'yes'\n",
    "  elif 'doesn&rsquo;t want' in x:\n",
    "    return 'no'\n",
    "  else:\n",
    "    return 'maybe'\n",
    "  \n",
    "data['has_kids'] = data['offspring'].apply(child_presense_transformer)\n",
    "data['has_kids'].fillna(data['has_kids'].mode().values[0], inplace=True)\n",
    "data['want_kids'] = data['offspring'].apply(want_child_transformer)\n",
    "data['want_kids'].fillna(data['want_kids'].mode().values[0], inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data[['has_kids', 'want_kids']].head(15).T"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['sign'].unique()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# def transform_\n",
    "spltd = data['sign'].str.split(' ', 1).transform(lambda x: [None, None] if type(x) != list else x)\n",
    "spltd = pd.DataFrame(spltd.tolist(), columns=['sign', 'astrology_respect']).fillna('not set')\n",
    "data['sign'] = spltd['sign']\n",
    "data['astrology_respect'] = spltd['astrology_respect']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['pets'].unique()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разобъем колонку на 2 одтельных для котов и собак"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cats_relations = ['has cats', 'likes cats', 'dislikes cats']\n",
    "dogs_relations = ['has dogs', 'likes dogs', 'dislikes dogs']\n",
    "data['pets'].fillna('not set', inplace=True)\n",
    "data['cats_relation'] = None\n",
    "data['dogs_relation'] = None\n",
    "for r in cats_relations:\n",
    "  data['cats_relation'][data['pets'].str.contains(r)] = r\n",
    "for r in dogs_relations:\n",
    "  data['dogs_relation'][data['pets'].str.contains(r)] = r\n",
    "  \n",
    "data[['cats_relation', 'dogs_relation']] = data[['cats_relation', 'dogs_relation']].fillna('not set')\n",
    "data[['cats_relation', 'dogs_relation']].head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 3. Первичный визуальный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь мы будем смотреть распределения и чистить выбросы в чисельных данных. Учитывая отношение социума к нашему таргету - возможно большое колличество выбросов как раз и будет таргетом (чтоб лишний раз не попадать в поиск). Проверим гипотезу в конце"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Посмотрим распределение чисельных данных\n",
    "sns.kdeplot(data[data['sex'] == 'm']['age'], color='blue').set_title('Распределение возраста')\n",
    "sns.kdeplot(data[data['sex'] == 'f']['age'], color='pink');"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.kdeplot(data[data['sex'] == 'm']['height'].dropna(), color='blue').set_title('Распределение роста')\n",
    "sns.kdeplot(data[data['sex'] == 'f']['height'].dropna(), color='pink');"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.countplot(data[data['sex'] == 'm']['income'].dropna(), color='blue').set_title('Распределение дохода')\n",
    "sns.countplot(data[data['sex'] == 'f']['income'].dropna(), color='pink');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно из графика - большинство респондентов не желает разглашать свой доход"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.boxplot(data['income'], data['age']).set_title('Доход в зависимости от возраста')\n",
    "plt.show()\n",
    "sns.violinplot(data=data, x='income', y='age', hue='sex', split=True);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим стереотип о том что богатые пользуются популярностью у противоположного пола"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['income'], data['status'], margins=True)\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (5,1)).T\n",
    "\n",
    "stacked = ct.iloc[:-1].stack().reset_index().rename(columns={0:'Доля респондентов'})\n",
    "sns.barplot(x=stacked['income'], y=stacked['Доля респондентов'], hue=stacked['status']);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Кажется доход совсем не влияет на счастье в личной жизни. А как насчет телосложения?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['body_type'], data['status'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (12,1)).T*100\n",
    "\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\").set_title('Процентное распределение телосложения внутри категорий статуса');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим - большинство респондентов - \"среднего телосложения\" :). Те кто состоит в браке не меньше следит за поддержанием формы, и среди них ощутимо больше процент людей с лишним весом. \n",
    "Посмотрим теперь на целевой признак, его распределение"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['sex'], data['orientation'], margins=True)\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (3,1)).T*100\n",
    "\n",
    "fig, ax = plt.subplots(1,1)\n",
    "stacked = ct.iloc[:-1].stack().reset_index().rename(columns={0:'Процент респондентов'})\n",
    "sns.barplot(x=stacked['sex'], y=stacked['Процент респондентов'], hue=stacked['orientation'], ax=ax);\n",
    "\n",
    "for p in ax.patches:\n",
    "  x=p.get_bbox().get_points()[:,0]\n",
    "  y=p.get_bbox().get_points()[1,1]\n",
    "  ax.annotate('{0:.1f}'.format(p.get_height()), (x.mean(), y), ha='center', va='bottom');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно гетеросексуальных мужчин и женщин приблизительно по 85%, но женщины нетрадиционной сексуальной ориентации предпочитают бисексуальность в то время как мужчины в основном - строго гомосексуальны.\n",
    "Еще один стереотип - среди богатых много людей нетрадиционной сексуальной ориентации. Проверим это"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['body_type'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (12,1)).T*100\n",
    "\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\").set_title('Процентное распределение телосложения внутри категорий ориентации');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Бисексуальные респонденты похоже значительно \"фигуристей\" остальных - выглядит как хороший признак. В то время как между геями и гетеросексуальными - особой разницы в теслосложении не наблюдается"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['income'], data['orientation'], margins=True)\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (3,1)).T\n",
    "\n",
    "stacked = ct.iloc[:-1].stack().reset_index().rename(columns={0:'Доля респондентов'})\n",
    "sns.barplot(x=stacked['income'], y=stacked['Доля респондентов'], hue=stacked['orientation']);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Частично миф подтвержден :) тем не менее среди людей с нихким доходом ЛГБТ все таки еще больше."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['status'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (5,1)).T*100\n",
    "\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\").set_title('Процентное распределение статуса внутри категорий ориентации');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Бисексуалы значительно реже холосты, чем остальные. Вероятно это связано с природой бисексуальности - есть мотивация пользоваться сайтом знакомств даже при наличии партнера.\n",
    "Мне нравятся бисексуалы - похоже их искать будет сравнительно просто.\n",
    "Проверим нашу изначальную гипотезу о том что среди выбросов будет много таргета."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# data['height'].quantile(0.995)*2.45\n",
    "print(\"Самый высокий и низкий респондент\", int(data['height'].max()*2.45), \"см и\", int(data['height'].min()*2.45), \"см\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "oldest_non_outlier = data['age'].quantile(0.995)\n",
    "smallest_non_outlier = data['height'].quantile(0.005)\n",
    "tallest_non_outlier = data['height'].quantile(0.995)\n",
    "print(oldest_non_outlier, 'лет - еще выглядит правдоподобно. Используем как максимальный \"не выброс\"')\n",
    "print(\"Самый высокий и низкий респондент\", int(data['height'].min()*2.45), \"см и\", int(data['height'].max()*2.45), \"см\")\n",
    "print(\"Возьмем пол перцентиля сверху и снизу по росту\", int(smallest_non_outlier*2.45), \"см и\", int(tallest_non_outlier*2.45), \"см\")\n",
    "outliers = data[data['height'].isnull() | \n",
    "                (data['age']>oldest_non_outlier) | \n",
    "                (data['height']<smallest_non_outlier) | \n",
    "                (data['height']>tallest_non_outlier)]\n",
    "print(\"Колличество выбросов\", outliers.shape[0], \"из них %.1f%% таргета\"%((outliers['orientation']!='straight').sum()*100/outliers.shape[0]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Согласно википедии карлики бывают ростом меньше 147см, 144 - вполне реалистично. 188 так же выглядит здраво. А вот предположение о выбросах не подтвердилось.\n",
    "В некоторых религиях ЛГБТ под запретом. Посмотрим распределение среди религий и среди серьезности отношения к религии"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ct = pd.crosstab(data['religion_confession'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (9,1)).T*100\n",
    "\n",
    "# f, (ax1, ax2) = plt.subplots(1, 2)\n",
    "ax1 = plt.subplot2grid((1,6),(0,0), colspan=4)\n",
    "\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax1) \\\n",
    ".set_title('Процентное распределение религии\\n внутри категорий ориентации');\n",
    "\n",
    "ax2 = plt.subplot2grid((1,6),(0,4), colspan=2, sharey=ax1)\n",
    "ct = pd.crosstab(data['religion_respect'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (4,1)).T*100\n",
    "\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax2) \\\n",
    ".set_title('Процентное распределение отношения к религии\\n внутри категорий ориентации');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Среди атеистов, и тех кто находит тему религии смешной значимо больше бисексуалов (шах и мат, аметисты xD). У ЛГБТ значимо меньше представителей аврамических религий, видимо церковь имеет свое влияние. Среди геев этот тренд наблюдается не так сильно как среди би. К сожалению геи слабо выделяются:(\n",
    "Смотрим отношение к наркотикам и алкоголю, сигаретам и диетам"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f, (ax1, ax2) = plt.subplots(1, 2)\n",
    "ct = pd.crosstab(data['drugs'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (3,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax1) \\\n",
    ".set_title('Процентное распределение отношения к наркотикам\\n внутри категорий ориентации');\n",
    "\n",
    "ct = pd.crosstab(data['drinks'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (6,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax2) \\\n",
    ".set_title('Процентное распределение отношения к алкоголю\\n внутри категорий ориентации');\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f, (ax1, ax2) = plt.subplots(2, 1)\n",
    "plt.tight_layout(pad=0.4, w_pad=4.5, h_pad=5)\n",
    "\n",
    "ct = pd.crosstab(data['diet_kind'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (6,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax1) \\\n",
    ".set_title('Процентное распределение диеты\\n внутри категорий ориентации');\n",
    "\n",
    "ct = pd.crosstab(data['smokes'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (5,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax2) \\\n",
    ".set_title('Процентное распределение отношения к курению\\n внутри категорий ориентации');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ну что ж - видно что бисексуалы имеют терпимое отношение к наркотикам, больше курят и ограничивают себя диетами. Кажется в признаках нет особого отличия для геев, просто на них корреляция слабее. \n",
    "\n",
    "Попробуем поискать корреляцию среди звезд - проверим как распредиляется ориентация среди разных знаков зодиака и отношения к астрологии :)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f, (ax1, ax2) = plt.subplots(2, 1)\n",
    "plt.tight_layout(pad=0.4, w_pad=4.5, h_pad=5)\n",
    "\n",
    "ct = pd.crosstab(data['sign'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (13,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax1) \\\n",
    ".set_title('Процентное распределение знака зодиака\\n внутри категорий ориентации');\n",
    "\n",
    "data['astrology_respect'] = data['astrology_respect'].str.replace('&rsquo;',\"'\")\n",
    "\n",
    "ct = pd.crosstab(data['astrology_respect'], data['orientation'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (4,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax2) \\\n",
    ".set_title('Процентное распределение отношения к астрологии\\n внутри категорий ориентации');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Наконец то гомосексуалисты отличились от би - они гораздо реже оставляют поле знака зодиака не заполненым :) А еще если вы рак (или весы) то у вас выше шанс быть геем. _Я не хочу обидить любителей астрологии, но мне кажется что на самом деле зависимость ориентации от знака зодиака абсолютно нулевая, и мы имеем дело с ложной корреляцией_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f, (ax1, ax2) = plt.subplots(2, 1)\n",
    "\n",
    "sns.kdeplot(data[data['orientation'] == 'straight']['age'], color='blue', ax=ax1).set_title('Распределение возраста')\n",
    "sns.kdeplot(data[data['orientation'] == 'gay']['age'], color='black', ax=ax1);\n",
    "sns.kdeplot(data[data['orientation'] == 'bisexual']['age'], color='pink', ax=ax1);\n",
    "sns.kdeplot(data[data['orientation'] == 'straight']['height'].dropna(), color='blue', ax=ax2).set_title('Распределение возраста')\n",
    "sns.kdeplot(data[data['orientation'] == 'gay']['height'].dropna(), color='black', ax=ax2);\n",
    "sns.kdeplot(data[data['orientation'] == 'bisexual']['height'].dropna(), color='pink', ax=ax2);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Похоже в росте и возрасте нет никакой зависимости, и все отклонения связаны с тем что би - в основному женщины а гие - мужчины"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 4. Закономерности, \"инсайты\", особенности данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "В большинстве своем даные выглядят очень хорошо, выбросов не так много, но и особых закономерностей о гомосексуалистах не видно. Зато бисексуалы достаточно отличаются. Есть зависимости в отношении к религиозной, социальной этике и спорту, некоторые другие зависимости, но внушает опасение тот факт что обычно геи и би имеют одинаковую зависимость от признака, но в разной степени - возможно их будет довольно сложно различать."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Особенностью даных есть некая \"текстовость\" - много категориальных признаков склеены в один. Есть целых 10 текстовых полей, много из них не заполнены. А чисельных признаков практически нет."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 5. Предобработка данных "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Большую часть предобработки даных мы уже сделали по ходу рассмотрения. Сейчас заполним пробелы и разобъем мультикатегориальные признаки на много бинарных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Посмотрим уникальные рассовые признаки. \n",
    "import re\n",
    "data['ethnicity'].fillna('not set', inplace=True)\n",
    "ethnicities = set()\n",
    "data['ethnicity'].str.split(', ').apply(lambda x: ethnicities.update(x));\n",
    "ethnicities "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for e in ethnicities:\n",
    "  data['ethnicity_' + e] = data['ethnicity'].str.contains(re.escape(e))\n",
    "#   Проделаем аналогичную обработку с языками"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['speaks'].fillna('not set', inplace=True)\n",
    "langs = set()\n",
    "data['speaks'].str.split(', ').apply(lambda x: langs.update(x));\n",
    "for l in langs:\n",
    "  data['speaks_' + l] = data['speaks'].str.contains(re.escape(l))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data[list(filter(lambda x: \n",
    "                 not x.startswith('speaks_') and \n",
    "                 not x.startswith('ethnicity_') and \n",
    "                 not x.startswith('essay'), data.columns))].count()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Заполним пропуски модой для тех признаков которые почти полностью заполнены\n",
    "data['body_type'].fillna(data['body_type'].mode().values[0], inplace=True)\n",
    "data['drinks'].fillna(data['drinks'].mode().values[0], inplace=True)\n",
    "data['smokes'].fillna(data['smokes'].mode().values[0], inplace=True)\n",
    "\n",
    "# education и job слишком сложные, оставим вместе с остальными 'not set'\n",
    "\n",
    "data['education'].fillna('not set', inplace=True)\n",
    "data['job'].fillna('not set', inplace=True)\n",
    "data['drugs'].fillna('not set', inplace=True)\n",
    "data['religion_confession'].fillna('not set', inplace=True)\n",
    "data['religion_respect'].fillna('not set', inplace=True)\n",
    "\n",
    "# выбросы вряд ли значат что человек который их заполнял ошибся на пару лет или дюймов - \n",
    "# в данном случае вернее будет заполнить даные медианными значениями\n",
    "data['height'].fillna(data['height'].median(), inplace=True)\n",
    "data['height'][data['height']>tallest_non_outlier] = data['height'].median()\n",
    "data['height'][data['height']<smallest_non_outlier] = data['height'].median()\n",
    "data['age'][data['age']>oldest_non_outlier] = data['age'].median()\n",
    "\n",
    "outliers = data[data['height'].isnull() | \n",
    "                (data['age']>oldest_non_outlier) | \n",
    "                (data['height']<smallest_non_outlier) | \n",
    "                (data['height']>tallest_non_outlier)]\n",
    "outliers.shape[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 6. Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Большая часть признаков либо категориальные либо бинарные(тоже категориальные но multilabel), но есть текст. Текст проще всего превратить в Tf-idf - будет sparse-матрица. Лучше всего справляется с категориальными признаками CatBoost, но он не работает с sparse-признаками, в которые мы можем превратить текст. Если применить к категориальным признакам One Hot Encoding - получится достаточно большая разреженая матрица. Деревянные методы не очень хорошо работают на таких даных(Учитывая tf-idf матрицу). Вероятно лучше всего взять в качестве бейслайн модели - логистическую регрессию. Линейные модели хорошо показывают себя на подобного рода признаках, и быстро обучаются, что удобно для эксперементов.\n",
    "После получения первого бейзлайна - попробуем превратить текст в doc2vec вектор, что снизит размерность до приемлимого уровня, и он больше не будет sparse - если существенной потери качества не будет - попробуем обучить CatBoost на категориях+doc2vec.\n",
    "Бустинг достаточно сложно использовать для тестирования фичей из-за обилия гиперпараметров и долгого обучения. Возможно нейронная сеть справилась бы с задачей точнее, и даже могла бы сама себе сделать doc2vec, но ~60k примеров - для нейронной сети может быть недостаточно(для обучения собственного doc2vec с нуля - совсем не достаточно. Для обучения нейронных сетей задачам nlp используются огромные корпуса текста по 1B слов). Кроме того - у нейронной сети очень короткое плато обучения. Переобучение наступает довольно быстро и часто незаметно, а характер обучения неудобен для многофолдовой крос-валидации."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Подитожим создание признаков:\n",
    "- tf-idf всего текста\n",
    "- doc2vec текста\n",
    "- OHE категориальных фич для бейслайн модели\n",
    "\n",
    "Клеим все essay в один и убираем разметку"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "essay = data[list(filter(lambda x: x.startswith('essay'), data.columns))].fillna(' ').replace('\\n|\\t',' ', regex=True)\n",
    "essay.to_csv('tmp_essay.csv', sep=' ', quotechar=' ',\n",
    "                       index=None, header=None)\n",
    "essay = pd.read_csv('tmp_essay.csv', sep='\\t', skip_blank_lines=False, quotechar='\\t', header=None)\n",
    "essay = essay[0].apply(lambda x: BeautifulSoup(x, 'lxml').get_text())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "le = LabelEncoder()\n",
    "y = le.fit_transform(data['orientation'])\n",
    "le.classes_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 7. Кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Временной зависимости в данных нет, но есть дизбаланс классов, поэтому валидироваться лучше на **StratifiedKFold**.\n",
    "На отложеную выборку уйдет 25%, остальное - 3-fold crossvalidation. На кросвалидации будем смотреть на полноту, если она растет выше дизбаланса в классах - посмотрим AUC."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "to_drop = ['diet', 'pets', 'speaks', 'religion', 'last_online',\n",
    "           'offspring', 'ethnicity', *filter(lambda x: x.startswith('essay'), data.columns)]\n",
    "\n",
    "data_train, data_test, essay_train, essay_test, y_train, y_test = \\\n",
    "  train_test_split(data.drop(columns=to_drop), essay, y, test_size=0.25, random_state=42)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, (ax1, ax2) = plt.subplots(1,2)\n",
    "\n",
    "ct = pd.crosstab(data_train['orientation'], data_train['sex'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (3,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax1).set_title('train');\n",
    "\n",
    "ct = pd.crosstab(data_test['orientation'], data_test['sex'], margins=True).T\n",
    "ct = ct[ct.columns[:-1]]/np.tile(ct['All'].values, (3,1)).T*100\n",
    "sns.heatmap(ct.iloc[:-1], fmt='.1f', annot=True, cmap=\"YlGnBu\", ax=ax2).set_title('test');\n",
    "\n",
    "# Check the distributions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "tfidf = TfidfVectorizer()\n",
    "essay_train_tfidf, essay_test_tfidf = tfidf.fit_transform(essay_train), tfidf.transform(essay_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Текстовые даные в tf-idf формате есть. Теперь отмасштабируем чисельные фичи, и получим OHE с категориальных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scale_features = ['age', 'height']\n",
    "scaler = StandardScaler()\n",
    "scaled_train, scaled_test = scaler.fit_transform(data_train[scale_features]), \\\n",
    "                                scaler.transform(data_test[scale_features])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def transform_label_encode(df_train, df_test, field):\n",
    "  le = LabelEncoder()\n",
    "  full_labled = pd.concat([df_train[field], df_test[field]])\n",
    "  full_labled = le.fit_transform(full_labled)\n",
    "  train_labled, test_labled = full_labled[:df_train.shape[0]], full_labled[df_train.shape[0]:]\n",
    "\n",
    "  ohe = OneHotEncoder(handle_unknown='ignore')\n",
    "  train_labled, test_labled = ohe.fit_transform(train_labled[:, np.newaxis]), ohe.transform(test_labled[:, np.newaxis])\n",
    "  return train_labled, test_labled\n",
    "# def transform_label_encode(df_train, df_test, field):\n",
    "#   lb = LabelBinarizer(sparse_output=True)\n",
    "#   return lb.fit_transform(df_train[field]), lb.transform(df_test[field])\n",
    "\n",
    "cat_features = ['body_type', 'drinks', 'drugs', 'education', 'income',\n",
    "               'job', 'location', 'sex', 'sign', 'smokes', 'status',\n",
    "               'diet_kind', 'diet_strenght', 'religion_confession', 'religion_respect',\n",
    "               'has_kids', 'want_kids', 'astrology_respect', 'cats_relation',\n",
    "               'dogs_relation']\n",
    "ohe_train, ohe_test = zip(*[transform_label_encode(data_train, data_test, f) for f in cat_features])\n",
    "\n",
    "bin_features = [*filter(lambda x : x.startswith('speaks_') or x.startswith('ethnicity_'), data_train.columns)]\n",
    "bina_train, bina_test = data_train[bin_features].astype(float).values, data_test[bin_features].astype(float).values"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Склеим все вместе, и получим огромную разреженую матрицу"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train = csr_matrix(hstack([scaled_train, bina_train, *ohe_train, essay_train_tfidf]))\n",
    "X_test = csr_matrix(hstack([scaled_test, bina_test, *ohe_test, essay_test_tfidf]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь сделаем из текста doc2vec, нам поможет pretrained модель из tensorflow-hub:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def maybe_make_embeddings(text_train, text_test):\n",
    "  if os.path.exists('embedding_essay_train.txt') and os.path.exists('embedding_essay_test.txt'):\n",
    "    embedding_out_train = np.loadtxt('embedding_essay_train.txt')\n",
    "    embedding_out_test =  np.loadtxt('embedding_essay_test.txt')\n",
    "    return embedding_out_train, embedding_out_test\n",
    "  tf.logging.set_verbosity(tf.logging.WARN)\n",
    "  tf.reset_default_graph() \n",
    "  graph = tf.Graph()\n",
    "  with tf.device('/gpu:0'), graph.as_default():\n",
    "    tf_X_content = tf.placeholder(tf.string, shape=(None))\n",
    "    embed = hub.Module(\"https://tfhub.dev/google/universal-sentence-encoder/1\")\n",
    "    embedding_op = embed(tf_X_content)\n",
    "    \n",
    "  embedding_out_train = []\n",
    "  embedding_out_test = []\n",
    "  batch_size=500\n",
    "\n",
    "  def text_2_embed(X, start):\n",
    "    embedding_out = []\n",
    "    max_idx = X.shape[0]\n",
    "    for i in range(0, max_idx, batch_size):\n",
    "      end_idx = i+batch_size\n",
    "      embedding_out.append(session.run(embedding_op, feed_dict={\n",
    "        tf_X_content:X[i:i+batch_size]\n",
    "      }))\n",
    "      if i%5000 == 0:\n",
    "        print(\"%.1fs passed, i: %d of %d\" % (time.time() - start, i, max_idx) )\n",
    "\n",
    "    return embedding_out\n",
    "\n",
    "  start = time.time()\n",
    "\n",
    "  with tf.Session(graph=graph) as session:\n",
    "    session.run([tf.global_variables_initializer(), tf.tables_initializer()])\n",
    "    print('Initialized')\n",
    "    print(\"%.1fs passed\" % (time.time() - start) )\n",
    "\n",
    "    embedding_out_train = text_2_embed(text_train.values, start)\n",
    "    embedding_out_test = text_2_embed(text_test.values, start)\n",
    "    \n",
    "  embedding_out_train, embedding_out_test = np.concatenate(embedding_out_train), np.concatenate(embedding_out_test)\n",
    "    \n",
    "  np.savetxt('embedding_essay_train.txt', embedding_out_train)\n",
    "  np.savetxt('embedding_essay_test.txt', embedding_out_test)\n",
    "  tf.reset_default_graph() \n",
    "  return embedding_out_train, embedding_out_test\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "d2v_train, d2v_test = maybe_make_embeddings(essay_train, essay_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17, multi_class='multinomial', solver='lbfgs')\n",
    "scorer=make_scorer(recall_score, average='micro')\n",
    "skf = StratifiedKFold(y_train)\n",
    "cross_val_score(logit, X_train, y_train, scoring=scorer, cv=skf)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из коробки точность ~0.88. Похоже это из-за разбалансировки классов. Попробуем подстроить параметр регуляризации"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 8. Построение кривых валидации и обучения "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "param_range = np.logspace(-2, 2, 6)\n",
    "train_scores, test_scores = validation_curve(\n",
    "    logit, X_train, y_train, param_name=\"C\", param_range=param_range,\n",
    "    cv=skf, scoring=scorer, n_jobs=2)\n",
    "train_scores_mean = np.mean(train_scores, axis=1)\n",
    "train_scores_std = np.std(train_scores, axis=1)\n",
    "test_scores_mean = np.mean(test_scores, axis=1)\n",
    "test_scores_std = np.std(test_scores, axis=1)\n",
    "\n",
    "plt.title(\"Validation Curve with Logit\")\n",
    "plt.xlabel(\"C\")\n",
    "plt.ylabel(\"Recall\")\n",
    "plt.ylim(0.0, 1.1)\n",
    "lw = 2\n",
    "plt.semilogx(param_range, train_scores_mean, label=\"Training score\",\n",
    "             color=\"darkorange\", lw=lw)\n",
    "plt.fill_between(param_range, train_scores_mean - train_scores_std,\n",
    "                 train_scores_mean + train_scores_std, alpha=0.2,\n",
    "                 color=\"darkorange\", lw=lw)\n",
    "plt.semilogx(param_range, test_scores_mean, label=\"Cross-validation score\",\n",
    "             color=\"navy\", lw=lw)\n",
    "plt.fill_between(param_range, test_scores_mean - test_scores_std,\n",
    "                 test_scores_mean + test_scores_std, alpha=0.2,\n",
    "                 color=\"navy\", lw=lw)\n",
    "plt.legend(loc=\"best\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Похоже что регуляризация в нашем случае минимально влияет на полноту"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure()\n",
    "plt.title('Learning Curve with Logit')\n",
    "plt.ylim(0.0, 1.1)\n",
    "plt.xlabel(\"Training examples\")\n",
    "plt.ylabel(\"Recall\")\n",
    "train_sizes, train_scores, test_scores = learning_curve(\n",
    "  logit, X_train, y_train, scoring=scorer,\n",
    "#   cv=skf, \n",
    "  n_jobs=2, verbose=1, random_state=17)\n",
    "train_scores_mean = np.mean(train_scores, axis=1)\n",
    "train_scores_std = np.std(train_scores, axis=1)\n",
    "test_scores_mean = np.mean(test_scores, axis=1)\n",
    "test_scores_std = np.std(test_scores, axis=1)\n",
    "plt.grid()\n",
    "\n",
    "plt.fill_between(train_sizes, train_scores_mean - train_scores_std,\n",
    "                 train_scores_mean + train_scores_std, alpha=0.1,\n",
    "                 color=\"r\")\n",
    "plt.fill_between(train_sizes, test_scores_mean - test_scores_std,\n",
    "                 test_scores_mean + test_scores_std, alpha=0.1, color=\"g\")\n",
    "plt.plot(train_sizes, train_scores_mean, 'o-', color=\"r\",\n",
    "         label=\"Training score\")\n",
    "plt.plot(train_sizes, test_scores_mean, 'o-', color=\"g\",\n",
    "         label=\"Cross-validation score\")\n",
    "plt.legend(loc=\"best\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "К сожалению увеличение колличества примеров не покажет роста производительности - мы уже впритык подобрались к близжайшей точке между трейном и валидейшном"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_cb = np.concatenate([data_train[[*bin_features, *cat_features]].values, scaled_train, d2v_train], axis=1)\n",
    "X_test_cb = np.concatenate([data_test[[*bin_features, *cat_features]].values, scaled_test, d2v_test], axis=1)\n",
    "\n",
    "X_train_part, X_valid, y_train_part, y_valid = train_test_split(X_train_cb, y_train, test_size=0.25, random_state=42)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat_idx = len(bin_features) + len(cat_features)\n",
    "D_train = catboost.Pool(X_train_part, y_train_part, cat_features=list(range(cat_idx)))\n",
    "D_val = catboost.Pool(X_valid, y_valid, cat_features=list(range(cat_idx)))\n",
    "D_test = catboost.Pool(X_test_cb, cat_features=list(range(cat_idx)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = dict()\n",
    "params['learning_rate'] = 0.01\n",
    "params['depth'] = 8\n",
    "params['l2_leaf_reg'] = 1\n",
    "params['rsm'] = .5\n",
    "model = catboost.CatBoostClassifier(iterations=500,\n",
    "                                    learning_rate=params['learning_rate'],\n",
    "                                    depth=int(params['depth']),\n",
    "                                    loss_function='MultiClass',\n",
    "                                    use_best_model=True,\n",
    "                                    eval_metric='AUC',\n",
    "#                                     task_type='GPU',\n",
    "                                    random_seed=17,\n",
    "                                    colsample_bylevel=0.7,\n",
    "                                    od_type='IncToDec',\n",
    "                                    od_pval=1e-3,\n",
    "                                    od_wait=20,\n",
    "                                    verbose=False\n",
    "                                    )\n",
    "model.fit(D_train, eval_set=D_val, verbose=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "_CatBoost очень странно работает для мультиклассовой классификации, ни одна eval__metric кроме AUC в целом не показывает значений отличных от 0_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 9. Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predict = model.predict_proba(D_test)\n",
    "classes = model.predict(D_test)\n",
    "print('Accuracy is %.3f' % accuracy_score(y_test, classes))\n",
    "print('Category\\tAUC\\tAccuracy')\n",
    "for i in range(3):\n",
    "  y_cls = y_test == i\n",
    "  print(\"%8s\\t%.3f\\t%.3f\"%(le.classes_[i], roc_auc_score(y_cls, predict[:,i]), accuracy_score(y_cls, classes == i)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17, C=6)\n",
    "logit.fit(X_train, y_train)\n",
    "predict = logit.predict_proba(X_test)\n",
    "classes = logit.predict(X_test)\n",
    "print('Accuracy is %.3f' % accuracy_score(y_test, classes))\n",
    "print('Category\\tAUC\\tAccuracy')\n",
    "for i in range(3):\n",
    "  y_cls = y_test == i\n",
    "  print(\"%8s\\t%.3f\\t%.3f\"%(le.classes_[i], roc_auc_score(y_cls, predict[:,i]), accuracy_score(y_cls, classes == i)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 10. Оценка модели с описанием выбранной метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Не смотря на все старания в подборе гиперпараметров CatBoost - линейная модель имеет преимущества в работе с разрежеными данными. ROC_AUC логистической регрессии оказался существенно выше, а тренировка - проще. ROC_AUC сглаживает ситуацию несбалансированости классов, но даже если взять просто процент правильно классифицированных примеров то логит справился лучше."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 11. Выводы "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Линейные модели, и логистическая регрессия в частности показывают хороший бейзлайн в задаче классификации, отлично работают с разрежеными данными и легко настраивается. К сожалению идея использования CatBoost не сработала - то ли текст признаков essay слишком удобно ложится в tfidf, то ли я просто не смог подобрать гиперпараметры. Имея в распоряжении значительно больше примеров - можно было бы построить нейросетевую модеть - она может работать не хуже логистической регрессии, но обучается сложнее."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
